리눅스 커널 (2) - 빌드와 설치, 주의사항

2019-11-14

커널 소스 구하기

http://www.kernel.org

https://github.com/torvalds/linux

커널 소스 설치

bzip2 형식 압축 해제
$ tar xvjf linux-x.y.z.tar.bz2

GNU zip 형식 압축 해제
$ tar xvzf linux-x.y.z.tar.gz

  • 커널 소스는 보통 /usr/src/linux에 설치된다. 여기 접근하려면 루트 권한 필요, 새 커널 설치 시에만 루트 권한 사용하여 접근하고 그때에도 이 디렉토리 내용은 건드리면 안된다.

패치

점증적 패치를 적용하려면,
$ patch -p1 < ../patch-x.y.z

커널 소스 트리

디렉토리 설명
arch 특정 아키텍처와 관련된 소스
block 블록 입출력 계층
crypto 암호화 API
Documentation 커널 소스 문서
drivers 장치 드라이버
firmware 특정 드라이버를 사용할 때 필요한 장치 펌웨어
fs 가상 파일시스템 및 개별 파일시스템
include 커널 헤더 파일
init 커널 시작 및 초기화 관련 코드
ipc 프로세스 간 통신 관련 코드
kernel 스케줄러와 같은 핵심 커널 서브시스템
lib 유틸리티 루틴
mm 메모리 관리 서브시스템 및 가상 메모리
net 네트워크 서브시스템
samples 예제, 데모 코드
scripts 커널을 빌드하는 데 사용하는 스크립트
security 리눅스 보안 모듈
sound 사운드 서브시스템
usr 초기 사용자 공간 코드 initramfs
tools 리눅스 개발에 유용한 도구
virt 가상화 기반 구조
  • 소스트리 최상위에 있는 파일 : COPYING 파일은 커널 저작권 파일, CREDITS 파일에는 커널 개발 기여 개발자 명단, MAINTAINERS 파일에는 커널 서브시스템과 드라이버를 관리하는 사람들 명단, Makefile은 커널의 기본 Makefile

커널 설정

커널 설정 옵션은 CONFIG으로 시작하는 CONFIG_FEATURE과 같은 형태
e.g.) CONFIG_SMP : SMP 지원 여부

두가지 혹은 세가지 설정 값(보통 드라이버의 경우 세 가지 선택이 가능)을 지님.

yes or no (or module)

module 값을 가지는 경우 해당 기능은 모듈 형태(동적으로 로드할 수 있는 별도 오브젝트)로 컴파일된다.

설정 옵션은 문자열이나 숫자가 될 수도 있음
이런 옵션은 빌드과정을 조절하는 데 사용하지 않음
전처리 매크로를 통해 커널 소스가 참조하는 값을 지정하는 데 사용
정적으로 배열 크기를 지정하는 옵션이 그러한 예

Ubuntu용으로 Canonical에서 제공하거나, Fedora용으로 Red Hat에서 제공하는 것과 같은 벤더 커널은 컴파일된 상태로 배포본에 들어 있다. 이런 커널에는 많이 사용하는 커널 기능이 모두 들어 있음.
거의 모든 드라이버를 모듈 형태로 컴파일함.
이렇게 하면, 모듈을 통해 다양한 하드웨어를 지원하는 기본 커널로 사용 가능
스스로 커널을 컴파일하고, 어떤 모듈을 포함시킬 것인지 제외할 것인지 배워나가야 한다.

커널은 설정을 조절하는 여러 가지 도구를 제공
가장 간단한 도구로 텍스트 기반의 명령행 도구가 있다

$ make config

이 도구는 각 옵션을 하나씩 돌아가면서 대화식으로 사용자에게 yes, no, (세 가지 선택 가능 시) module 중에서 어떤 선택을 할지 물어본다.
이는 시간이 아주 오래 걸리는 일이므로 시간당 급여를 받는 상황이 아니라면 ncurses 라이브러리 사용하는 그래픽 환경의 도구를 이용하는 편이 좋다.

$ make menuconfig

또는 gtk+ 기반의 그래픽 환경 도구를 이용할 수도 있다.

$ make gconfig

이 두가지 도구는 다양한 설정 옵션을 '프로세서 형식 및 기능' 등의 항목으로 분류해 보여줌.
각 분류항목을 오가면서 커널 옵션을 확인하고 값을 변경할 수 있다.

다음 명령은 아키텍처에 맞는 기본 설정을 만들어 줌

$ make defconfig

이렇게 만들어진 기본 값은 다소 임의적이기는 하지만(i386의 경우 리누스가 사용하는 설정 값이라는 소문 있음.)
커널을 설정해본 적이 없는 경우에는 좋은 출발점이 될 수 있을 것이다.
빨리 빌드해서 실행해보고 싶다면, 이 명령을 실행한 다음 하드웨어에 필요한 옵션이 설정되었는지 확인하자.

옵션 설정은 커널 소스 트리의 최상위에 있는 .config 파일에 저장된다. (대부분의 커널 개발자가 그렇듯이)
이 파일을 직접 수정하는 편이 쉽게 느껴질 수 있음.
이 파일에서 설정 옵션을 찾아 값을 변경하는 일이 그다지 어렵지 않기 때문
설정파일을 직접 변경한 경우나, 기존의 설정 파일을 새 커널 트리에 사용하는 경우에는 다음 명령을 이용해 설정 확인 및 갱신 가능

$ make oldconfig

커널을 빌드하기 전에 항상 이 명령을 실행해야 한다.
CONFIG_IKCONFIG_PROC 설정 옵션을 사용하면 전체 커널 설정 파일을 압축해서 /proc/config.gz 파일에 저장한다.
이를 이용하면 새 커널을 빌드할 때 현재 사용하는 설정을 쉽게 복사 가능
현재 커널이 이 옵션을 사용하고 있다면, 다음과 같은 방법으로 /proc에 있는 설정 파일을 이용해 새 커널을 빌드할 수 있다.

$ zcat /proc/config.gz > .config
$ make oldconfig

어떤 방식으로든 커널 설정을 마쳤다면, 다음 명령으로 간단하게 커널을 빌드할 수 있다.

$ make

이전 버전 커널과는 달리 2.6에서는 의존성 정보 자동 관리됨. => 커널 빌드하기 전 make dep 명령 실행 필요 없음
또한, bzImage와 같은 특정 빌드 형식을 지정하거나 모듈을 별도로 빌드하지 않아도 된다.
Makefile이 기본적인 모든 것을 처리함

빌드 메시지 최소화

빌드 시 쏟아지는 메시지를 최소화하면서도 경고나 오류 메시지를 놓치지 않으려면 make의 출력을 리다이렉트한다.

$ make > ../detritus

빌드 과정의 출력 메시지를 보고 싶다면 저장된 파일을 보면 된다.
하지만 경고와 오류 메시지는 표준 에러 장치로 출력되므로 대개 이 파일을 볼 일은 없다.
대신 다음을 실행

$ make > /dev/null

이렇게 하면 모든 불필효한 출력을 다시는 돌아오지 못하는 커다란 하수구인 /dev/null로 보낸다.

빌드 작업을 동시에 여러 개 실행

make 프로그램에는 빌드 과정을 여러 개의 병렬 작업으로 분리해 주는 기능이 있다.
각각의 작업은 별도로 동시에 실행되므로 다중 프로세서 시스템에서는 빌드 속도를 크게 향상시킬 수 있다.
커다란 소스를 빌드하는 경우에는 입출력 대기 시간(프로세스가 입출력 요청이 완료되기를 기다리는 시간)이 차지하는 비중이 높으므로 이 기능을 이용해 프로세스 이용도 높일 수 있음

Makefile의 의존성 정보가 잘못되어 있는 경우가 너무나 많아 기본적으로는 make는 하나의 작업만 생성한다.
잘못된 의존성 정보하에서 여러 개의 작업을 생성하면 다른 작업에 영향을 미쳐 전체 빌드 과정에서 오류가 발생할 수 있기 때문 커널 Makefile의 의존성 정보는 정확하므로 여러 개의 작업을 생성해도 문제가 발생하지 않는다.
다중 make 작업을 통해 커널을 빌드하려면 다음 명령을 이용

$ make -jn

여기서 n은 생성할 작업의 개수 의미
일반적으로 프로세서 하나당 하나 또는 두 개의 작업을 생성하는 것이 적당
예를 들어 16코어 장비라면 다음과 같이 실행 가능

$ make -j32 > /dev/null

distcc 또는 ccache와 같은 도구 사용 시 커널 빌드 시간을 극적으로 줄일 수 있다.

새 커널 설치

커널을 빌드하고 나면 커널 설치해야 함
설치 방법은 아키텍처 및 부트 로더에 따라 다르므로 커널 이미지를 어디에 복사하고, 해당 이미지로 부팅하려면 어떻게 해야 하는지 부트 로더 사용법 참고
새 커널이 문제를 일으킬 수 있으므로 안전한 것으로 확인된 커널 한두 개를 사용할 수 있도록 해두는 것을 잊지 말 것
예를 들어, grub을 사용하는 x86 시스템이라면 arch/i386/boot/bzImage 파일을 /boot 디렉토리 안에 vmlinuz-version 같은 이름으로 넣어두고, /boot/grub/grub.conf 파일을 수정해 새 커널을 위한 항목을 추가
LILO를 사용해 부팅하는 시스템이라면 /etc/lilo.conf 파일을 편집하고 lilo 명령을 실행한다.

모듈 설치는 자동화되어 있고, 아키텍처에 따른 차이가 없음
루트 권한으로 다음 명령을 실행하기만 하면 된다.

% make modules_install

이렇게 하면 컴파일된 모듈들이 정해진 위치인 /lib/modules 디렉토리에 설치된다.
빌드 과정에서 커널 소스 트리 최상위에 System.map 파일이 만들어진다.
이 파일에는 각 커널 심볼의 시작 주소의 위치를 찾을 수 있는 테이블이 들어 있다.
디버깅 시에 이 정보를 이용해 메모리 주소 값을 그에 해당하는 함수나 변수 이름으로 변환해서 보여줄 수 있음

다른 성질의 야수

리눅스 커널은 일반적인 사용자 공간 애플리케이션과 다른 몇 가지 독특한 특징 존재
이런 차이가 커널 개발 작업을 사용자 프로그램 개발 작업과 다르게 만듬

다른 규칙이 적용됨
당연해 보이는 차이점도 있지만, 명확해 보이지 않는 차이점도 존재

  • 커널은 C 라이브러리나 표준 C 헤더 파일을 사용할 수 없다.
  • 커널은 GNU C를 사용한다.
  • 커널에는 사용자 공간에서와 같은 메모리 보호 기능이 없다.
  • 커널은 부동소수점 연산을 쉽게 실행할 수 없다.
  • 커널은 프로세스당 고정된 작은 크기의 스택을 사용한다.
  • 커널은 비동기식 인터럽트를 지원하며, 선점형이며, 대칭형 다중 프로세싱을 지원하므로 커널 내에서는 동기화 및 동시성 문제가 매우 중요하다.
  • 이식성이 중요하다.

lib와 표준 헤더 파일을 사용할 수 없음

사용자 공간 애플리케이션과 달리, 커널은 표준 C 라이브러리(또는 그 외의 라이브러리)와도 링크되지 않음
주요한 이유는 속도와 크기 때문
전체 C 라이브러리, 아니면 그 중요 일부분이라도 커널 입장에서는 너무 크고 비효율적
대신 일반적인 libc 함수의 상당수는 커널 안에 구현되어 있으므로 안심해도 좋음
예를 들어, 보통의 문자열 처리 함수는 lib/string.c에 들어 있음
<linux/string.h> 헤더 파일 추가 시 해당 함수 사용 가능

  • 여기서 헤더 파일은 커널 소스 트리 안에 있는 커널 헤더 파일 일컬음.
    커널 소스에서 외부 라이브러리를 사용할 수 없는 것과 마찬가지로 커널 소스는 외부 헤더 파일을 사용 불가
    기본 파일은 커널 소스 트리 최상위의 include/ 디렉토리에 있음. 예를 들어, <linux/inotify.h>에 해당하는 파일은 커널 소스 트리의 include/linux/inotify.h에 있다. 아키텍처별 특정 헤더 파일은 커널 소스 트리의 arch/(아키텍처)/include/asm 디렉토리에 있음.
    e.g. x86 아키텍처 => arch/x86/include/asm 디렉토리에 있음
    이 곳의 헤더 파일을 사용하는 경우에는 <asm/ioctl.h>처럼 asm/ 접두사만 사용하면 된다.

빠진 함수 중 가장 익숙한 함수 printf()
커널 코드는 printf()를 사용할 수 없는 대신 printk() 함수를 제공,
이 함수는 아주 익숙한 printf 함수와 거의 같은 방식으로 작동
printk 함수는 형식화한 문자열을 커널 로그 버퍼에 복사하며, 이 메시지는 보통 syslog 프로그램이 처리
사용법은 printf() 함수와 유사

printk("Hello wolrd! A string '%s' and an integer '%d'\n", str, i);

printf 함수와 printk 함수 사이의 주목할 만한 차이점 하나는 printk 함수에는 우선순위 플래그를 줄 수 있다는 점이다.
이 플래그를 통해 syslogd(데몬 프로그램)가 커널 메시지를 어느 곳에 표시할지를 결정할 수 있다.
이 기능을 사용하는 예를 들어보면 다음과 같다.

printk(KERN_ERR "this is an error!\n");

KERN_ERR과 출력 메시지 사이에 쉼표가 없는 점에 주의
이는 의도된 표현 방식
우선순위 플래그는 문자형으로 표시된 선처리 지시자로 컴파일 과정에서 출력 메시지와 합쳐진다.

GNU C

리눅스 커널은 유닉스 커널과 마찬가지로 C로 프로그램 되어 있음.
커널은 엄격한 ANSI C로 되어있지 않고, 대신 개발자들은 필요하다고 생각되는 곳에 gccGNU Compiler Collection (커널 및 리눅스 시스템에 있는 C로 작성된 다른 거의 모든 프로그램 컴파일 시 사용하는 C 컴파일러)가 제공하는 다양한 언어 확장 기능 사용

커널 개발자들은 C 언어의 ISO C99과 GNU C 확장 기능 모두 사용
gcc의 기능을 충분히 지원하는 최근 버전의 Intel C 컴파일러로도 리눅스 커널을 컴파일할 수 있지만,
이런 점들로 인해 리눅스 커널은 gcc 편향되 있음
지원하는 가장 오래된 gcc 버전은 3.2이며, 4.4 이후 버전을 권장
ISO C99는 C 언어의 공식적인 개정판으로 기존과 큰 차이 없으므로 다른 코드에서도 서서히 이용되는 추세
표준 ANSI C에 비해 더 생소한 확장 기능은 GNU C가 제공하는 확장 기능들

인라인 함수

C99와 GNU C 모두 인라인 함수를 지원
인라인 함수는 이름으로 짐작할 수 있듯이 각 함수 호출이 일어나는 자리의 줄 안에 삽입되는 함수
이 기능을 통해 함수 호출과 반환 시에 발생하는 부가 비용(레지스터를 저장, 복원 등)을 제거 가능
컨파일러가 함수를 호출하는 코드와 호출되는 코드를 하나로 보고 최적화 가능해 더 정교한 최적화가 가능
함수의 내용이 호출하는 자리에 복사되어 들어가기 때문에 코드의 크기가 커지며,
이로 인해 메모리 사용량과 명령어 캐시 사용량이 늘어남
커널 개발자들은 일부 실행시간이 중요한 함수에 대해 인라인 함수 사용

큰 함수를 인라인으로 만드는 일은 해당 함수가 특별히 자주 사용되거나 실행시간에 극히 민감한 경우가 아니라면 피하는 것 권장

인라인 함수는 함수 정의부분에 static과 inline 지시어를 사용해 선언

static inline void wolf(unsigned long tail_size)

인라인 함수 정의는 함수를 사용하기 전에 해야 함. 그렇지 않으면 컴파일러가 함수를 인라인으로 만들 수 없음.
인라인 함수를 헤더 파일에 두고 사용하는 것이 일반적. 인라인 함수는 static으로 지정했으므로 외부에서 사용 불가.
인라인 함수가 한 파일에서만 사용된다면 해당 파일의 최상단에 둘 수 있음.

커널에서는 형type 보호 및 가독성 등의 이유로 복잡한 매크로를 사용하는 것보다 인라인 함수를 사용하는 것을 선호

인라인 어셈블리

gcc C 컴파일러는 일반적인 C 함수 안에 어셈블리 명령을 삽입하는 기능을 제공
이 기능은 특정 시스템 아키텍처에서만 사용하는 커널 소스에서 사용

인라인 어셈블리 이용 시 asm() 컴파일러 지시자 사용
e.g. x86 프로세서의 rdtsc 명령 실행해서 타임스탬프 레지스터(tsc) 내용 받아오는 코드

unsigned int low, high;
asm volatile("rdtsc" : "=a" (low), "=d" (high));
/* 이제 low와 high에는 각각 64비트 tsc의 하위 32비트, 상위 32비트의 값이 들어간다. */

리눅스 커널은 C와 어셈블리를 혼합해서 작성되어 있는데, 어셈블리는 주로 하부 아키텍처와 관련되어 있거나 빠른 속도를 요하는 부분에서 사용
대부분의 커널 코드는 C로 작성 되어 있음

분기 구문 표시

gcc C 컴파일러는 분기 시에 어느 쪽이 발생할 가능성이 높은지를 이용해 분기 구문을 최적화하는 내장 지시자를 가지고 있음
컴파일러는 이 지시자를 이용해 분기를 예측 가능
커널은 이 지시자를 사용하기 쉽게 likely()와 unlikely()라는 매크로로 만들어 사용

다음과 같은 코드에서

if (error) {
	/* ... */
}

다음과 같은 방식으로 이 분기가 거의 실행되지 않음을 표시 가능

/* error 값이 거의 항상 0일 것으로 생각 가능 */if (unlikely(error)) {
	/* ... */
}

반면, 항상 실행될 것 같은 경우

/* success 값은 거의 항상 0이 아닐 것으로 간주 */
if (likely(error)) {
	/* ... */
}

분기의 방향이 거의 대부분 알려진 한 방향으로만 일어나는 경우, 또는 다른 경우를 무시하고 한 가지 경우에 대해서만 최적화가 필요한 경우 이 지시자 사용
매우 중요한 사항으로, 이 지시자 오용 시에 심각한 성능저하
일반적으로는 앞에서 봤듯이 오류가 발생하는 상황에서 unlikely()와 likely()를 사용
특별히 예외를 처리하기 위해 if 문을 사용하는 경우 많음,
커널에서도 주로 unlikely() 지시자 사용

메모리 보호 없음

사용자 공간 애플리케이션이 메모리 접근을 잘못하면, 커널은 오류를 탐지해 SIGSEGV 시그널을 보내고 프로세스 종료.
하지만 커널이 메모리 접근을 잘못한 경우에는 이를 제어하기 어려움
커널에서의 메모리 침범은 중대한 커널 오류인 oops를 발생시킴
NULL 포인터 참조와 같이 잘못된 메모리 접근을 해서는 안 된다는 것은 당연하지만 커널에서 그 위험성이 훨씬 큼.

또한 커널 메모리는 페이징 기능 사용 불가
커널에서 사용하는 모든 메모리는 실제 물리적인 메모리에 해당
나중에 커널에 새로운 기능을 추가해야 한다면 이 점 명심해야 함

부동 소수점 쉽게 사용 불가

사용자 공간 애플리케이션이 부동 소수점 연산을 사용할 경우, 커널이 정수와 부동 소수점 연산 모드 전환 관리
부동 소수점 연산 이용 시 커널이 해야 하는 일은 아키텍처에 따라 다르지만,
보통 커널이 트랩을 받아 정수 연산에서 부동 소수점 연산 모드로 전환하는 방식으로 동작

사용자 공간과 달리 커널은 자신의 트랩을 받을 수 없어 깔끔한 부동 소수점 전환 기능 이용 불가
커널 내에서 부동 소수점을 사용하려면 수동으로 부동 소수점 레지스터를 저장하고 복원하는 등 잡다한 일을 직접 해야 함.
사용하지 말아야 함. 아주 드문 경우 제외, 커널에서 부동 소수점 연산 사용 안 함.

작은 고정 크기의 스택

사용자 공간에서는 스택에 커다란 구조체나 수 천 개 크기의 배열 등 많은 변수 정적 할당 가능
사용자 공간에는 동적으로 확장 가능한 커다란 스택이 있기 때문에 가능
DOS 같은 구식 OS 경우 사용자 공간에서도 고정 크기 스택 쓰기 때문에 문제 될 수는 있음

커널이 사용하는 스택은 크지도 않고, 동적으로 확장 불가
커널 스택의 정확한 크기는 아키텍처에 따라 다름
x86 아키텍처의 경우 : 컴파일 시에 4KB 또는 8KB로 정할 수 있음
관습적으로 커널 스택은 두 페이지로 구성하므로, 이는 커널 스택의 크기가 32비트 아키텍처에서는 8KB, 64비트 아키텍처에서는 16KB로 고정 되 있으며, 바꿀 수 없음을 의미
각 프로세스별로 각자의 스택이 할당됨

동기화와 동시성

커널은 경쟁 상태race condition에 놓이기 쉬움.
단일 스레드의 사용자 공간 애플리케이션과 달리, 커널은 공유 자원에 대한 동시 접근 허용해야 하므로 경쟁 방지하기 위한 동기화 필요

다음과 같은 경우 존재

  • 리눅스는 선점형 멀티태스킹 OS. 커널의 프로세스 스케줄러에 의해 프로세스의 실행 순서가 조정됨. 커널은 이 작업들 간의 동기화를 책임져야 함.
  • 리눅스는 대칭형 다중 프로세서(SMP) 지원. 따라서 적절한 보호 장치 없으면 동일한 자원에 하나 이상의 프로세스가 동시에 접근하는 커널 코드 실행할 수 있음.
  • 현재 실행하고 있는 코드와 상관없이 비동기식으로 인터럽트가 발생. 따라서 적절한 보호 장치 없을 시 자원 사용 도중 인터럽트가 발생하고 인터럽트 핸들러에서 같은 자원에 접근하는 상황 발생 가능.
  • 리눅스 커널은 선점형. 따라서 적절한 보호 장치가 없을 시 같은 자원에 접근하는 다른 커널 코드가 실행 중인 커널 코드를 선점하는 일이 발생 가능.

경쟁 상태 해결하는 전형적인 해법은 스핀락spinlock이나 세마포어semaphore를 이용하는 것.

이식성의 중요성

사용자 공간 애플리케이션은 이식성이 그다지 중요하지 않을 수 있음
리눅스는 이식성이 좋은 OS이며, 그 특성 유지해야 함.
이는 아키텍처 독립적인 C 코드가 여러 다양한 시스템에서 컴파일되고 실행되어야 한다는 의미이며,
커널 소스 트리에서 아키텍처 독립적인 코드는 특정 시스템에 의존적인 코드와 적절하게 분리되어 있어야 한다는 뜻.

엔디언 중립성, 64비트 지원, 워드 및 페이지 크기 지정 등 몇 가지만 설명해도 방대함.

결론

커널에는 고유한 특징 존재
커널은 자신만의 규칙을 사용하며 전체 시스템을 관리하는 만큼 분명 그에 따르는 위험도 큼
하지만 리눅스 커널의 복잡도와 진입장벽은 여타 대규모 소프트웨어 프로젝트와 질적으로 크게 다르지 않음

리눅스 개발에 있어 가장 중요한 단계는 커널이 두려움의 존재가 아니라는 것을 깨닫는 것.